package jxl;

import jxl.utils.HttpsUtil;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.net.ssl.HttpsURLConnection;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLDecoder;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.text.DecimalFormat;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


public class Job {
    private static final Log log = LogFactory.getLog(Job.class);
    private int fileSize;
    private String fileName;
    private int connectTimeout = 150000;
    private int readTimeout = 200000;
    private String url;
    private String storeDir;
    private int taskNum = 50;
    private String jobId;
    private int[] startIndexes;
    private int[] endIndexes;
    private int[] progress;
    private Task[] tasks;

    private File storeDirFile;
    private File dtDirFile;
    private File localFile;
    private ThreadLocal<RandomAccessFile> rafLocalTl;
    private ThreadLocal<RandomAccessFile> rafOffsetTl;
    private CountDownLatch latch;
    private ProgressThread pt;


    public Job(String url, String storeDir) throws IOException {
        this.url = url;
        this.storeDir = storeDir;
        this.startIndexes = new int[taskNum];
        this.endIndexes = new int[taskNum];
        this.progress = new int[taskNum];
        this.tasks = new Task[taskNum];
        this.latch = new CountDownLatch(taskNum);
        this.jobId = UUID.randomUUID().toString().replaceAll("-", "");
        this.rafLocalTl = new ThreadLocal<RandomAccessFile>();
        this.rafOffsetTl = new ThreadLocal<RandomAccessFile>();
        this.pt = new ProgressThread();
    }


    public void startJob(int count) throws Exception {
        long start = System.currentTimeMillis();
        String s = "当前开始下载第"+count+"个文件...";
        System.out.println(s);
        log.info(s);
        boolean j = fetchFileMetaInfo();
        if (j) {
            /*  以下下载实体   */
            log.info("下载参数配置开始");
                assignTasks();
            log.info("下载参数配置结束，其他参数配置开始");
                createFiles();
            log.info("其他参数配置结束，创建多线程下载开始");
                startTasks();
            log.info("创建多线程下载结束，开启线程");
                openProgressThread();
            System.out.println("开始下载第 "+count+" 个文件..."+fileName);
            log.info("开始下载第 "+count+" 个文件..."+fileName);
                waitForCompeletion();
            /*  以上下载实体   */
            long end = System.currentTimeMillis();
            String str = "";
            long time = end - start;
            int a, b, c, d;
            a = (int) time / 1000 / 60 / 60 / 24;//天
            if (a > 0) {
                str = str + a + "天";
            }
            b = (int) (time - a * 1000 * 60 * 60 * 24) / 1000 / 60 / 60;//时
            if (b > 0) {
                str = str + b + "小时";
            }
            c = (int) (time - a * 1000 * 60 * 60 * 24 - b * 1000 * 60 * 60) / 1000 / 60;//分
            if (c > 0) {
                str = str + c + "分钟";
            }
            d = (int) (time - a * 1000 * 60 * 60 * 24 - b * 1000 * 60 * 60 - c * 1000 * 60) / 1000;
            if (d > 0) {
                str = str + d + "秒";
            }
            System.out.println("全程耗时约为" + str);
            log.info("全程耗时约为" + str);
        } else {
            throw new Exception("第"+count+"个文件下载失败");
        }
    }


    private void openProgressThread() {
        this.pt.start();
    }


    private void waitForCompeletion() throws Exception {
        latch.await();
        deleteFiles();
        pt.join();
    }


    private void deleteFiles() {
        if (dtDirFile != null) {
            File[] subFiles = dtDirFile.listFiles();
            for (File subFile : subFiles) {
                subFile.delete();
            }
            dtDirFile.delete();
        }
    }


// 1.fetch file size and file name

    private boolean fetchFileMetaInfo() throws IOException {
        HttpURLConnection connection = null;
        try {
            connection = createConnection();
//            connection.setRequestProperty("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9");
//            connection.setRequestProperty("Accept-Language", "zh-CN,zh;q=0.9");
//            connection.setRequestProperty("User-Agent"," Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36");
            connection.setRequestMethod("GET");
        } catch (Exception e) {
            log.error("获取文件下载服务器资源连接异常，异常详情："+e.getMessage());
            throw new IOException("获取文件下载服务器资源连接异常");
        }
        if (connection.getResponseCode() == 200 || connection.getResponseCode() == 206) {
            this.fileSize = connection.getContentLength();//获取整个下载的文件的长度，计算开启多少线程下载
            String disposition = connection.getHeaderField("Content-Disposition");
            try {
                if (disposition == null) {
                    parseFileNameFromUrl(url);
                } else {
                    parseFileNameFromDisposition(disposition);
                }
            }catch (Exception e){
                log.error("获取文件名异常，"+e.getMessage());
            }
            if (this.fileName == null || this.fileSize < 0) {
                return false;
            }
            String s = "找到文件资源，长度为" + fileSize / 1024 / 1024 + "MB,资源名称为" + fileName;
            System.out.println(s);
            log.info(s);
            return true;
        }
        return false;
    }


    private void parseFileNameFromUrl(String url) throws UnsupportedEncodingException {
        this.fileName = url.substring(url.lastIndexOf("/") + 1, url.length());
        if (this.fileName.contains("%")) {
            this.fileName = URLDecoder.decode(this.fileName, "UTF-8");
        }
    }


    private void parseFileNameFromDisposition(String disposition) throws UnsupportedEncodingException {
        Pattern pattern = Pattern.compile(".+filename=\"(.+?)\".*");
        Matcher matcher = pattern.matcher(disposition);
        if (matcher.matches()) {
            this.fileName = new String(matcher.group(1).getBytes("ISO-8859-1"), "UTF-8");
        } else {
            parseFileNameFromUrl(url);
        }
    }


    public HttpURLConnection createConnection() throws Exception {
        URL urlObj = new URL(url);
        HttpURLConnection connection = (HttpURLConnection) urlObj.openConnection();
        if (url.contains("https")){
            ((HttpsURLConnection) connection).setSSLSocketFactory(HttpsUtil.getSSLSocketFactory());
        }
        connection.setConnectTimeout(connectTimeout);
        connection.setReadTimeout(readTimeout);
        connection.setRequestProperty("Accept-Charset", "UTF-8");
        connection.setRequestProperty("contentType", "UTF-8");
        return connection;
    }


// 2.assign every task start index and end index out of the file

    private void assignTasks() throws IOException {
        for (int i = 0; i < taskNum; i++) {
            int size = fileSize / taskNum;
            int startIndex = i * size;
            int endIndex = i == taskNum - 1 ? fileSize - 1 : i * size + size - 1;
            this.startIndexes[i] = startIndex;
            this.endIndexes[i] = endIndex;
        }
    }


// 3.create the local file and temp directory

    private void createFiles() throws IOException {
        storeDirFile = new File(storeDir);
        storeDirFile.mkdirs();
        localFile = new File(storeDirFile, fileName);
        dtDirFile = new File(storeDirFile, "." + jobId);
        dtDirFile.mkdirs();
        if (!localFile.exists()) {
            RandomAccessFile raf = new RandomAccessFile(localFile, "rw");
            raf.setLength(fileSize);
            raf.close();
        }
    }


// 4.let the task start to do their work

    private void startTasks() throws IOException {
        for (int i = 0; i < taskNum; i++) {
            Task task = new Task(this, i);
            tasks[i] = task;
            task.start();
        }
    }


    private int totalReadBytes() {
        int totalReadBytes = 0;
        for (int i = 0; i < progress.length; i++) {
            totalReadBytes += progress[i];
        }
        return totalReadBytes;
    }


    public int[] getStartIndexes() {
        return startIndexes;
    }


    public int[] getEndIndexes() {
        return endIndexes;
    }


    public void writeLocalFile(int startIndex, byte[] buf, int off, int len) throws IOException {
        if (rafLocalTl.get() == null) {
            RandomAccessFile raf = new RandomAccessFile(localFile, "rw");
            rafLocalTl.set(raf);
        }
        RandomAccessFile raf = rafLocalTl.get();
        raf.seek(startIndex);
        raf.write(buf, off, len);
    }


// 5.let task to report their progress

    public void reportProgress(int index, int readBytes) {
        progress[index] = readBytes;
    }


    public void closeTaskResource(int index) throws IOException {
        RandomAccessFile raf = rafLocalTl.get();
        if (raf != null) {
            raf.close();
        }
        raf = rafOffsetTl.get();
        if (raf != null) {
            raf.close();
        }
    }


    public void commitOffset(int index, int offset) throws IOException {
        File offsetFile = new File(dtDirFile, String.valueOf(index));
        if (rafOffsetTl.get() == null) {
            RandomAccessFile raf = new RandomAccessFile(offsetFile, "rw");
            rafOffsetTl.set(raf);
        }
        RandomAccessFile raf = rafOffsetTl.get();
        raf.seek(0);
        raf.writeInt(offset);
    }


    public int readOffset(int index) throws IOException {
        File offsetFile = new File(dtDirFile, String.valueOf(index));
        if (offsetFile.exists()) {
            RandomAccessFile raf = new RandomAccessFile(offsetFile, "rw");
            raf.seek(0);
            int offset = raf.readInt();
            raf.close();
            return offset;
        }
        return 0;
    }


    public void reStartTask(int index) throws IOException {
        Task task = new Task(this, index);
        tasks[index] = task;
        task.start();
    }


    public void taskFinished() {
        latch.countDown();
    }


    private class ProgressThread extends Thread {
        private DecimalFormat decimalFormat = new DecimalFormat();


        public void run() {
            decimalFormat.applyPattern("0.0");
            int a = 0;
            int averageSpeed = 0;
            int maxSpeed = 0;
            int minSpeed = 0;
            String maxSpeedStr = "";
            String minSpeedStr = "";
            while (true) {
                ++a;
                try {
                    int endPointX = totalReadBytes();
                    TimeUnit.SECONDS.sleep(1);
                    int endPointY = totalReadBytes();
                    int waitSeconds = 1;
                    while (endPointY - endPointX == 0) {
                        TimeUnit.SECONDS.sleep(1);
                        waitSeconds++;
                        endPointY = totalReadBytes();
                    }
                    int speed = (endPointY - endPointX) / waitSeconds;
                    averageSpeed = averageSpeed + speed;
                    String speedStr = speed > 1024 ? speed/1024+"kb/s":speed+"b/s";
                    if (a == 1) {
                        maxSpeed = speed;
                        maxSpeedStr = speedStr;
                        minSpeed = speed;
                        minSpeedStr = speedStr;
                    }
                    if (speed > maxSpeed) {
                        maxSpeed = speed;
                        maxSpeedStr = speedStr;
                    }
                    if (speed < minSpeed) {
                        minSpeed = speed;
                        minSpeedStr = speedStr;
                    }
                    String percent = decimalFormat.format(endPointY * 100.0 / fileSize);
                    int remainSeconds = (fileSize - endPointY)/speed;
                    String dateTimes = null;
                    long days = remainSeconds / ( 60 * 60 * 24);
                    long hours = (remainSeconds % ( 60 * 60 * 24)) / (60 * 60);
                    long minutes = (remainSeconds % ( 60 * 60)) /60;
                    long seconds = remainSeconds % 60;
                    if(days>0){
                        dateTimes = days + "天" + hours + "小时" + minutes + "分钟" + seconds + "秒";
                    }else if(hours>0){
                        dateTimes = hours + "小时" + minutes + "分钟" + seconds + "秒";
                    }else if(minutes>0){
                        dateTimes = minutes + "分钟" + seconds + "秒";
                    }else{
                        dateTimes = seconds + "秒";
                    }
                    System.out.print("\r当前进度: " + percent + "%\t速度: " + speedStr + " 估计还需要: " + dateTimes + "  \t\t");
                    log.info("当前进度: " + percent + "%\t速度: " + speedStr + " 估计还需要: " + dateTimes + "  \t\t");
                    if("100.0".equals(percent)) {
                        break;
                    }
                } catch (InterruptedException e) {
//                    e.printStackTrace();
                }
            }
            averageSpeed = averageSpeed / a;
            String speedStr = averageSpeed > 1024 ? averageSpeed / 1024 + "kb/s" : averageSpeed + "b/s";
            System.out.println("\n下载完成,平均速度:" + speedStr + ",最大速度为:" + maxSpeedStr + ",最小速度为:" + minSpeedStr);
            log.info("下载完成,平均速度:" + speedStr + ",最大速度为:" + maxSpeedStr + ",最小速度为:" + minSpeedStr);
        }
    }
}
